###########################################
# FP2021/2022 @ IST                       #
# Projeto 1 - BDB                         #
# Alberto Abad                            #
#                                         # 
# Proposta solucao (sem comentarios       #
# nem documentacao)                       #
###########################################


# TASK1: Documentacao
def corrigir_palavra(word):
    stack = list()
    for c in word:
        if stack and stack[-1].upper() == c.upper() and c != stack[-1]: #react
            del stack[-1]
        else:
            stack += c

    return ''.join(stack)


def eh_anagrama(w1, w2):
    return ''.join(sorted(list(w1.lower()))) == ''.join(sorted(list(w2.lower())))


def corrigir_doc(text):
    def palavras_iguais(w1, w2):
        return w1.lower() == w2.lower()

    ascii_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    if isinstance(text, str) and len(text) >=1 and all((c in ascii_letters + ' ') for c in text):
        words = [corrigir_palavra(w) for w in text.split()]
        new_text = ''

        while len(words) > 0:
            w = words[0]
            new_text += (w + ' ')
            j = 1
            while j < len(words):
                if not palavras_iguais(w, words[j]) and eh_anagrama(w, words[j]):
                    del words[j]
                else:
                    j = j + 1

            words = words[1:]

        return new_text[:-1]

    raise ValueError('corrigir_doc: argumento invalido')


# TASK2: Obter PIN 
def obter_posicao(cad, num):
        table = {1: {'C': 1, 'B': 4, 'E': 1, 'D': 2},
                 2: {'C': 2, 'B': 5, 'E': 1, 'D': 3},
                 3: {'C': 3, 'B': 6, 'E': 2, 'D': 3},
                 4: {'C': 1, 'B': 7, 'E': 4, 'D': 5},
                 5: {'C': 2, 'B': 8, 'E': 4, 'D': 6},
                 6: {'C': 3, 'B': 9, 'E': 5, 'D': 6},
                 7: {'C': 4, 'B': 7, 'E': 7, 'D': 8},
                 8: {'C': 5, 'B': 8, 'E': 7, 'D': 9},
                 9: {'C': 6, 'B': 9, 'E': 8, 'D': 9},
                 }

        return table[num][cad]


def obter_digito(cad, num):
    for c in cad:
        num = obter_posicao(c, num)

    return num


def obter_pin(seq):
    if isinstance(seq, tuple) and 4 <= len(seq) <= 10 \
            and all(isinstance(cad, str) and len(cad) >= 1 for cad in seq) and \
            all((c in 'CBED') for cad in seq for c in cad):
        num, res = 5, ()
        for cad in seq:
            num = obter_digito(cad, num)
            res += (num,)

        return res

    raise ValueError('obter_pin: argumento invalido')


# TASK 3: Verificacao checksum
def eh_entrada(t):
    ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
    return isinstance(t, tuple) and len(t) == 3 \
           and isinstance(t[0], str) and len(t[0]) >= 1 and all(c in (ascii_lowercase + '-') for c in t[0]) and \
           isinstance(t[1], str) and len(t[1]) == 7 and t[1][0] == '[' and t[1][-1] == ']' and \
           all(c in ascii_lowercase for c in t[1][1:-1]) \
           and isinstance(t[2], tuple) and len(t[2]) >= 2 and all((type(n) == int and n>0) for n in t[2])


def validar_cifra(cifra, check):
    def conta_letras(cifra):
        d = {}
        for c in cifra:
            if c not in d and c != '-':
                d[c] = cifra.count(c)

        return d

    d = conta_letras(cifra)
    return check[1:-1] == ''.join(l for l, v in sorted(d.items(), key=lambda x:x[1]*(1+ord('z')) - ord(x[0]), reverse=True)[:5])


def filtrar_bdb(lst):
    if isinstance(lst, list) and len(lst) >= 1 and all(eh_entrada(e) for e in lst):
        new_lst = list()
        for e in lst:
            if not validar_cifra(e[0], e[1]):
                new_lst.append(e)

        return new_lst

    raise ValueError('filtrar_bdb: argumento invalido')


# TASK 4: Decifrar (cifra shift)
def obter_num_seguranca(t):
    num = t[0] - t[1] if t[0] - t[1] > 0 else t[1] - t[0]
    for i in range(len(t)-1):
        for j in range(i+1, len(t)):
            tmp = (t[i] - t[j]) if t[i] > t[j] else (t[j] - t[i])
            num = num if num < tmp else tmp

    return num


def decifrar_texto(cad, num):
    def decifrar_char(c, num):
        return chr((ord(c) - ord('a') + num) % (ord('z') - ord('a') + 1) + ord('a'))

    return ''.join(' ' if c == '-' else decifrar_char(c, num + (1 if i%2==0 else -1)) for i, c in enumerate(cad))


def decifrar_bdb(lst):
    if isinstance(lst, list) and len(lst) >= 1 and all(eh_entrada(e) for e in lst):
        new_lst = list()
        for e in lst:
            new_lst.append(decifrar_texto(e[0], obter_num_seguranca(e[2])))

        return new_lst

    raise ValueError('decifrar_bdb: argumento invalido')



# TASK 5
def eh_utilizador(entry):
    def eh_regra(r):
        ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
        return isinstance(r, dict) and len(r) == 2 and 'vals' in r and 'char' in r and isinstance(r['vals'], tuple) \
               and len(r['vals']) == 2 and type(r['vals'][0]) == int and type(r['vals'][1]) == int \
               and r['vals'][0] > 0 and r['vals'][1] > 0 and r['vals'][0] <= r['vals'][1] and isinstance(r['char'], str) \
               and len(r['char']) == 1 and r['char'] in ascii_lowercase

    return isinstance(entry, dict) and len(entry) == 3 and \
        'name' in entry and isinstance(entry['name'], str) and len(entry['name']) > 0 and \
        'pass' in entry and isinstance(entry['pass'], str) and len(entry['pass']) > 0 and \
        'rule' in entry and eh_regra(entry['rule'])


def eh_senha_valida(senha, rule):
    def numero_vocais(senha):
        return sum(senha.count(c) for c in 'aeiou')

    def seq_carateres(senha):
        for i in range(len(senha)-1):
            if senha[i] == senha[i+1]:
                return True
        return False

    c = rule['char']
    lo, hi = rule['vals']

    return lo <= senha.count(c) <= hi and numero_vocais(senha) >= 3 and seq_carateres(senha)


def filtrar_senhas(lst):
    new_lst = list()
    if isinstance(lst, list) and len(lst) >= 1:
        for entry in lst:
            if not eh_utilizador(entry):
                raise ValueError('filtrar_senhas: argumento invalido')

            if not eh_senha_valida(entry['pass'], entry['rule']):
                new_lst.append(entry['name'])

        return sorted(new_lst)

    raise ValueError('filtrar_senhas: argumento invalido')


